package elsim.entities

import java.util.List
import java.util.Comparator;

import org.apache.log4j.Logger;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Iterables;
import com.google.common.collect.Ordering;
import com.google.common.primitives.Ints;

import elsim.common.ClockControllable
import elsim.common.ElSimConstants;
import elsim.common.Tickable;


public class Lift extends ClockControllable implements Tickable {
  private static final Logger LOG = Logger.getLogger(Lift.class);

  public static enum LiftState {
    BROKEN,
    OPERATIONAL
  }

  public static enum LiftOperationalState {
    MOVING,
    LOADING_PASSENGERS,
    WAITING
  }

  @Override
  public String toString() {
    StringBuilder builder = new StringBuilder();
    builder.append("Lift [liftId=");
    builder.append(liftId);
    builder.append(", passengers=");
    builder.append(passengers);
    builder.append(", pendingRequests=");
    builder.append(pendingRequests);
    builder.append(", state=");
    builder.append(state);
    builder.append(", position=");
    builder.append(position);
    builder.append(", direction=");
    builder.append(direction);
    builder.append(", computeFinalDestination()=");
    builder.append(computeFinalDestination());
    builder.append("]");
    return builder.toString();
  }

  private final int liftId
  private final List<Passenger> passengers = []
  private final List<Passenger> pendingRequests = []

  LiftState state = LiftState.OPERATIONAL

  private int loadPassengersCounter = 0

  /**
   * Where lift is right now
   */
  int position

  /**
   * Lift can go either up, down or stand stationary.
   */
  Direction direction = Direction.STATIONARY

  public Lift(int liftId) {
    this.liftId = liftId
  }

  public void addPassenger(Passenger p) {
    passengers.add(p)
  }

  public void addPendingRequest(Passenger p) {
    pendingRequests.add(p)
  }

  @Override
  void tick(int clock) {
    if (updateState() == LiftOperationalState.MOVING) {
      LOG.debug("MOVING " + toString())
      position += this.direction.get()
    } else {
    }
  }

  public int getPosition() {
    return position
  }

  public Direction getDirection() {
    return direction;
  }

  public List<Passenger> getPassengers() {
    return passengers
  }

  public List<Passenger> getPendingRequests() {
    return pendingRequests
  }

  public LiftState getLiftState() {
    return state
  }

  public void brokeLift() {
    liftState = LiftState.BROKEN
  }

  public void fixLift() {
    liftState = LiftState.OPERATIONAL
  }

  public boolean isOperational() {
    return liftState == LiftState.OPERATIONAL
  }

  /**
   * Main self-controlling method of lift. 
   */
  @VisibleForTesting LiftOperationalState updateState() {
    if (loadPassengersCounter > 0) {
      --loadPassengersCounter
      return LiftOperationalState.LOADING_PASSENGERS
    }

    // if lift just started moving, then by definition it
    // shouldn't load or unload any passengers
    if (shouldMoveToPendingPassenger()) {
      return LiftOperationalState.MOVING
    }

    // check should load/unload passengers
    if (shouldLoadPassengers()) {
      loadPassengersCounter = ElSimConstants.TIME_TO_LOAD_PASSENGERS
      return LiftOperationalState.LOADING_PASSENGERS
    }

    updateDirection()

    if (direction == Direction.STATIONARY) {
      return LiftOperationalState.WAITING
    } else {
      return LiftOperationalState.MOVING
    }
  }

  @VisibleForTesting boolean shouldLoadPassengers() {
    boolean result = false
    if (this.position % ElSimConstants.SECONDS_PER_FLOOR == 0) {
      // we are at floor level
      def newPendingRequests = []
      for (Passenger p : pendingRequests) {
        if (p.from == this.position) {
          LOG.debug("Loading passenger " + p.toString())
          passengers.add(p)
          result = true
        } else {
          newPendingRequests.add(p)
        }
      }

      pendingRequests.clear()
      pendingRequests.addAll(newPendingRequests)

      def newPassengers = []
      for (Passenger p : passengers) {
        if (p.to != this.position) {
          newPassengers.add(p)
        } else {
          LOG.debug("Unloading passenger " + p.toString())
          result = true
        }
      }

      passengers.clear()
      passengers.addAll(newPassengers)
    }

    return result
  }

  @VisibleForTesting boolean shouldMoveToPendingPassenger() {
    if (this.direction == Direction.STATIONARY) {
      if (pendingRequests.size() > 0) {
        Passenger head = pendingRequests.head()
        return updateDirectionFromPassenger(head, head.from)
      }
    }

    return false
  }

  @VisibleForTesting boolean updateDirectionFromPassenger(Passenger passenger, int direction) {
    if (direction != this.position) {
      if (direction > this.position) {
        this.direction = Direction.UP
      } else {
        this.direction = Direction.DOWN
      }
      return true
    } else {
      return false
    }
  }

  @VisibleForTesting Direction updateDirection() {
    if (passengers.size() == 0) {
      if (pendingRequests.size() == 0) {
        this.direction = Direction.STATIONARY
      }
    } else {
      Passenger p = passengers.head()
      updateDirectionFromPassenger(p, p.to)
    }

    return this.direction
  }

  /**
   * Final destination of the lift is the last point it is 
   * going to get if follows current direction and current
   * set of request (passengers). It is defined by most 
   * distant point it needs to get which may be either 
   * most distant point at which last passenger exits, or
   * it may be the destination specified by some previous 
   * passenger call. 
   * 
   * TODO: we must also consider the destination to which passenger
   * which waits for this lift at 'destination' wants to go.
   */
  public int computeFinalDestination() {
    if (passengers.size() == 0) {
      if (pendingRequests.size() == 0) {
        return 0
      } else {
        return PassengerAlgorithms.mostDistantPassenger(position, pendingRequests).to
      }
    } else {
      if (pendingRequests.size() == 0) {
        return PassengerAlgorithms.mostDistantPassenger(position, passengers).to
      } else {
        int mostDistantPassengerDest =
            PassengerAlgorithms.mostDistantPassenger(position, passengers).to
        int mostDistantPendingDest =
            PassengerAlgorithms.mostDistantPassenger(position, pendingRequests).to

        if (direction == Direction.UP) {
          return Math.max(mostDistantPendingDest, mostDistantPassengerDest)
        } else {
          return Math.min(mostDistantPendingDest, mostDistantPassengerDest)
        }
      }
    }
  }
}
